﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.XPath;

namespace InfoPathHelpers
{
    public class FormInfo
    {

        /// <summary>
        /// Loads a compiled schema set using the default MainDataSource schema location from the form.
        /// </summary>
        /// <param name="form">The form to load the schema from.</param>
        /// <returns>A comiled schema set containing the schema for the form's MainDataSource.</returns>
        public static XmlSchemaSet LoadSchemaSet(FormContext formContext)
        {
            return LoadSchemaSet(formContext, "mySchema.xsd");
        }

        /// <summary>
        /// Loads a compiled schema set for the schema with the supplied name within the form template.
        /// </summary>
        /// <param name="form">The form to load the schema from.</param>
        /// <param name="schemaName">The name of the schema to load.</param>
        /// <returns>A comiled schema set containing the schema from the form matching the supplied <paramref name="schemaName"/>.</returns>
        public static XmlSchemaSet LoadSchemaSet(FormContext formContext, string schemaName)
        {
            schemaName = Path.GetFileNameWithoutExtension(schemaName) + ".xsd";

            XmlSchema Schema = null;

            try
            {
                XmlSchema.Read(formContext.Template.OpenFileFromPackage(schemaName), null);
            }
            catch (FileNotFoundException)
            {
                throw new ArgumentException("No schema with that name could be located within the form template", "schemaName");
            }
            XmlSchemaSet SchemaSet = new XmlSchemaSet();
            SchemaSet.Add(Schema);
            SchemaSet.Compile();
            return SchemaSet;
        }

        public static XPathDocument OpenSchemaFile(FormContext formContext)
        {
            return OpenSchemaFile(formContext, "mySchema.xsd");
        }

        public static XPathDocument OpenSchemaFile(FormContext formContext, string schemaName)
        {
            schemaName = Path.GetFileNameWithoutExtension(schemaName) + ".xsd";

            try
            {
                return new XPathDocument(formContext.Template.OpenFileFromPackage(schemaName));
            }
            catch (FileNotFoundException)
            {
                throw new ArgumentException("No schema with that name could be located within the form template", "schemaName");
            }
        }

        public static List<NodeInfo> GetAllNodeInfo(FormContext formContext)
        {
            return GetAllNodeInfo(formContext, null);
        }

        public static List<NodeInfo> GetAllNodeInfo(FormContext formContext, string rootElementName)
        {
            var NodeInformation = new List<NodeInfo>();

            XPathDocument Document = OpenSchemaFile(formContext);
            var NamespaceManager = GetNamespaceManager(Document);
            // All attributes in the schema have no namespace.
            var AttributeNamespace = string.Empty;
            var Navigator = Document.CreateNavigator();

            XPathNodeIterator NodeReferences;
            // Select all nodes (of any type) with a "ref" attribute.
            // This will give us a list of all distinct elements in the form, because InfoPath 
            // adds an element with a "ref" attribute for every field, group, or choice.
            if (string.IsNullOrEmpty(rootElementName))
            {
                NodeReferences = Navigator.Select("//*[@ref]");
            }
            else
            {
                NodeReferences = Navigator.Select("//*[@name = '" + rootElementName + "']//*[@ref]");
            }

            foreach (XPathNavigator NodeReference in NodeReferences)
            {
                var NodeInfo = GetNodeInfo(formContext, NodeReference, Navigator, NamespaceManager);
                if (NodeInfo != null)
                {
                    NodeInformation.Add(NodeInfo);
                }
            }
            return NodeInformation;
        }

        public static NodeInfo GetNodeInfo(FormContext formContext, string localName)
        {

            XPathDocument Document = OpenSchemaFile(formContext);
            var NamespaceManager = GetNamespaceManager(Document);
            // All attributes in the schema have no namespace.
            var AttributeNamespace = string.Empty;
            var Navigator = Document.CreateNavigator();

            // Select nodes with a "ref" attribute matching the supplied node.
            var NodeReference = Navigator.SelectSingleNode("//*[@ref='" + localName + "']");
            if (NodeReference == null)
            {
                throw new ArgumentException("An element with the supplied name could not be loacted in the schema.", "localName");
            }

            return GetNodeInfo(formContext, NodeReference, Navigator, NamespaceManager);
        }

        private static NodeInfo GetNodeInfo(FormContext formContext, XPathNavigator nodeReference, XPathNavigator documentRoot, IXmlNamespaceResolver namespaceManager)
        {
            var AttributeNamespace = string.Empty;
            // InfoPath schemas are always organised with the first element being the root.
            // Selecting the first element node and getting the "name" 
            // attribute give us the name of the root element.
            var RootElement = documentRoot.SelectSingleNode("//xsd:element", namespaceManager)
                .GetAttribute("name", AttributeNamespace);

            // Create a new NodeInfo using the Name of the current node. (from the "ref" attribute).
            NodeInfo NodeInfo = new NodeInfo(nodeReference.GetAttribute("ref", AttributeNamespace));

            // If the "maxOccurs" element is present then the element is repeating.
            var MaxOccursAttribute = nodeReference.GetAttribute("maxOccurs", AttributeNamespace);
            NodeInfo.IsRepeating = !string.IsNullOrEmpty(MaxOccursAttribute);

            // Get the type definition for this node.
            // Every "ref" attribute references a type definition by name 
            // so each one can be found using the "name" attribute.
            var NodeDefinition = documentRoot
                .SelectSingleNode("//*[@name='" + NodeInfo.LocalName + "']");

            if (NodeDefinition == null)
            {
                // If the node definition could not be located it is probably from a different schema which is unsupported.
                return null;
            }

            // If the nillable node is present then the node is nillable.
            var NillableAttribute = NodeDefinition.GetAttribute("nillable", AttributeNamespace);
            NodeInfo.IsNillable = !string.IsNullOrEmpty(NillableAttribute);

            // IF the "type" attribute is present we can get the type of the node.
            var TypeAttribute = NodeDefinition.GetAttribute("type", AttributeNamespace);
            // If it is not present it may be because the node is complex (e.g. has attibutes in which case
            // we can try looking for the "base" attribute.
            if (string.IsNullOrEmpty(TypeAttribute))
            {
                

                var BaseAttribute = NodeDefinition.SelectSingleNode("*//@base");
                TypeAttribute = BaseAttribute == null ? string.Empty : BaseAttribute.Value;
            }

            if(string.IsNullOrEmpty(TypeAttribute))
            {
                var XHTMLNode = NodeDefinition.SelectSingleNode("*//*[@namespace = 'http://www.w3.org/1999/xhtml']");
                TypeAttribute = XHTMLNode == null ? string.Empty : "String";
            }

            if (!string.IsNullOrEmpty(TypeAttribute))
            {
                // If we get a value for type we need to remove the 
                // namespace so it can be parsed successfully.
                TypeAttribute = TypeAttribute.Replace("xsd:", "");

                // Try and parse the value of the "type" attribute to an XmlTypeCode.
                try
                {
                    XmlTypeCode XmlType = (XmlTypeCode)Enum.Parse(typeof(XmlTypeCode), TypeAttribute, true);
                    NodeInfo.XmlType = XmlType;
                }
                catch (ArgumentException)
                {
                    NodeInfo.XmlType = XmlTypeCode.None;
                }
            }

            if (NodeDefinition.LocalName == "attribute")
            {
                // The node is an "attibute".
                NodeInfo.InfoPathType = InfoPathNodeType.Attribute;
            }
            else if (NodeDefinition.Select("*/xsd:sequence", namespaceManager).Count != 0 && NodeInfo.XmlType == XmlTypeCode.None)
            {
                // If the node contains a "sequence" element it is a group.
                NodeInfo.InfoPathType = InfoPathNodeType.Group;
            }
            else if (NodeDefinition.Select("*/xsd:choice", namespaceManager).Count != 0)
            {
                // If the node contains a "choice" element it is a choice.
                NodeInfo.InfoPathType = InfoPathNodeType.ChoiceGroup;
            }
            else
            {
                // If none of the above are true the node is an element.
                NodeInfo.InfoPathType = InfoPathNodeType.Element;
            }

            string XPath;

            if (NodeInfo.InfoPathType == InfoPathNodeType.Attribute)
            {
                XPath = "/@";
            }
            else
            {
                XPath = "/";
            }
            XPath += NodeInfo.Name;

            var CurrentNode = nodeReference.Clone();

            while (true)
            {
                var Name = string.Empty;
                while (string.IsNullOrEmpty(Name))
                {
                    CurrentNode.MoveToParent();
                    Name = CurrentNode.GetAttribute("name", AttributeNamespace);
                }

                if (Name != RootElement)
                {
                    Name = "my:" + Name;
                    XPath = "/" + Name + XPath;
                    CurrentNode = documentRoot.SelectSingleNode("//*[@ref='" + Name + "']");
                    continue;
                }
                else
                {
                    NodeInfo.XPath = "/my:" + RootElement + XPath;
                    break;
                }
            }

            CurrentNode = nodeReference.Clone();
            while (CurrentNode.MoveToPrevious())
            {
                if (CurrentNode.NodeType == XPathNodeType.Element)
                {
                    NodeInfo.PreviousSiblingName = CurrentNode.GetAttribute("ref", AttributeNamespace);
                    break;
                }
            }

            var Node = formContext.SelectSingleNode(NodeInfo.XPath);
            if (Node != null)
            {
                NodeInfo.DefaultElementXml = "\r" + Node.OuterXml.PadLeft(Node.OuterXml.Length + NodeInfo.GetDepth(), '\t');

                // Check if there are any attibutes on the node (not inlcuding xsi:nil attributes).
                if (Node.HasAttributes)
                {
                    foreach (XPathNavigator Attribute in Node.Select("./@*", formContext.NamespaceManager))
                    {
                        if (Attribute.Prefix != "xsi")
                        {
                            NodeInfo.HasAttibutes = true;
                            break;
                        }
                    }
                }
            }

            
            
            return NodeInfo;
        }

        /// <summary>
        /// Gets the full XPath expression of the supplied <paramref name="node"/>.
        /// </summary>
        /// <param name="node">The node for which to get the XPath.</param>
        /// <returns>The full XPath expression for the supplied <paramref name="node"/>.</returns>
        public static string GetFullXPath(XPathNavigator node)
        {
            // Clone the node so we don't mess with the position of the one passed in.
            node = node.Clone();
            var XPath = node.Name;
            // Keep moving to the parent node and prepending it's name
            // to the XPath until the root is reached.
            while (node.MoveToParent())
            {
                XPath = node.Name + "/" + XPath;
            }
            return XPath;
        }

        private static XmlNamespaceManager GetNamespaceManager(XPathDocument Document)
        {
            // Create a navigator and use the nametable to create a NamespaceManger.
            var Navigator = Document.CreateNavigator();
            var NamespaceManager = new XmlNamespaceManager(Navigator.NameTable);

            // Move to the root element which declares the namespaces.
            Navigator.MoveToFollowing(XPathNodeType.Element);
            // Get all namespaces.
            var Namespaces = Navigator.GetNamespacesInScope(XmlNamespaceScope.All);
            // Add all namespaces to the NamespaceManager.
            foreach (var KeyValuePair in Namespaces)
            {
                NamespaceManager.AddNamespace(KeyValuePair.Key, KeyValuePair.Value);
            }

            return NamespaceManager;
        }

        internal static Type GetCLRType(XmlTypeCode xmlType)
        {
            switch (xmlType)
            {
                case XmlTypeCode.Integer:
                    return typeof(Int32);
                case XmlTypeCode.Boolean:
                    return typeof(Boolean);
                case XmlTypeCode.DateTime:
                    return typeof(DateTime);
                case XmlTypeCode.Double:
                    return typeof(Double);
                case XmlTypeCode.String:
                default:
                    return typeof(String);
            }
        }
    }

    public class NodeInfo
    {
        private string _Name;

        public NodeInfo() { }

        public NodeInfo(string name)
        {
            Name = name;
        }

        public string Name
        {
            get { return _Name; }
            set
            {
                _Name = value;
                Namespace = _Name.Split(':')[0];
                LocalName = _Name.Split(':')[1];
            }
        }
        public string LocalName { get; private set; }
        public string XPath { get; set; }
        public string Namespace { get; private set; }
        public XmlTypeCode XmlType { get; set; }
        public InfoPathNodeType InfoPathType { get; set; }
        public bool IsRepeating { get; set; }
        public bool IsNillable { get; set; }
        public bool HasAttibutes { get; set; }
        public string ParentName
        {
            get
            {
                var XPathElements = XPath.Split('/');
                if (XPathElements.Length <= 2)
                {
                    return null;
                }
                else
                {
                    return XPathElements[XPathElements.Length - 2];
                }
            }
        }
        public string PreviousSiblingName { get; set; }
        public string DefaultElementXml { get; set; }

        public int GetDepth()
        {
            return XPath.Split('/').Length - 1;
        }
    }

    

    public enum InfoPathNodeType
    {
        Element,
        Attribute,
        Group,
        ChoiceGroup,
    }
}
